package com.tony.jrp.core;

import com.tony.jrp.config.JRPLocation;
import com.tony.jrp.config.JRPProxy;
import com.tony.jrp.config.ProxyType;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.datagram.DatagramSocket;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.*;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.proxy.handler.ProxyHandler;
import io.vertx.httpproxy.HttpProxy;
import io.vertx.httpproxy.ProxyOptions;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 反向代理服务实现
 */
public class ReverseProxyVerticle extends AbstractVerticle {
    private static final Logger logger = LoggerFactory.getLogger(ReverseProxyVerticle.class);
    public static final String HTTPS = "https";
    public static final String SEPARATOR = "/";
    @Getter
    private JRPProxy proxy;
    /**
     * http(s)路由创建对象
     */
    Router proxyRouter = null;
    /**
     * 代理路径和route记录
     */
    private final Map<String, Route> locationRouteMap = new HashMap<>();

    public ReverseProxyVerticle(JRPProxy proxy) {
        this.proxy = proxy;
    }

    @Override
    public void start(Promise<Void> startPromise) {
//        "id":1,"name":"demo","type":"HTTP","location":"/","proxy_pass":"https://www.baidu.com/"
        Future<Void> startFuture;
        switch (proxy.getType()) {
            case HTTP:
            case HTTPS:
                startFuture = startHTTPProxy();
                break;
            case TCP:
                startFuture = startTCPProxy();
                break;
            case UDP:
                startFuture = startUDPProxy();
                break;
            case SOCKS4:
                startFuture = startSOCKS4Proxy();
                break;
            case SOCKS5:
                startFuture = startSOCKS5Proxy();
                break;
            default:
                startPromise.complete();
                return;
        }
        startFuture.onComplete((done) -> startPromise.complete());
    }

    private Future<Void> startHTTPProxy() {
        Promise<Void> promise = Promise.promise();
        proxyRouter = Router.router(vertx);
        List<JRPLocation> locationList = proxy.getLocations();
        newRoute(locationList);
        HttpServerOptions serverOptions = new HttpServerOptions();
        boolean ssl = ProxyType.HTTPS == proxy.getType();
        if (ssl) {
            //SelfSignedCertificate ssc = SelfSignedCertificate.create();自签名
            serverOptions.setSsl(true);
            serverOptions.setKeyCertOptions(new PemKeyCertOptions().addKeyPath(proxy.getCert_key()).addCertPath(proxy.getCert_cer()));
        }
        HttpServer httpServer = vertx.createHttpServer(serverOptions);
        httpServer.requestHandler(proxyRouter).listen(proxy.getListen()).onComplete((rs) -> {
            String locations = locationList.stream().map(JRPLocation::getLocation).collect(Collectors.joining(","));
            if (rs.succeeded()) {
                logger.info("Proxy locations [{}] of type {} start on port {} success!", locations, proxy.getType().name(), proxy.getListen());
                promise.complete();
            } else {
                logger.error("Proxy locations [{}] of type {} start on port {} error:{}!", locations, proxy.getType().name(), proxy.getListen(), rs.cause().getMessage(), rs.cause());
                promise.fail(rs.cause());
            }
        });
        return promise.future();
    }

    /**
     * 更新代理地址信息
     *
     * @param newProxy 代理信息
     */
    public void updateProxy(JRPProxy newProxy) {
        this.proxy = newProxy;
    }

    /**
     * 更新http(s)代理地址信息
     *
     * @param newProxy 代理信息
     */
    public Future<Void> updateRoute(JRPProxy newProxy) {
        Promise<Void> promise = Promise.promise();
        try {
            List<JRPLocation> locationList = proxy.getLocations();
            Map<String, JRPLocation> jrpLocationMap = locationList.stream().collect(Collectors.toMap(JRPLocation::toString, r -> r));
            //需要删除的路由
            locationRouteMap.forEach((key, value) -> {
                if (!jrpLocationMap.containsKey(key)) {
                    value.remove();
                    locationRouteMap.remove(key);
                }
            });
            //需要添加的路由
            List<JRPLocation> addLocationList = jrpLocationMap.entrySet().stream().filter((entry) -> !locationRouteMap.containsKey(entry.getKey())).map(Map.Entry::getValue).collect(Collectors.toList());
            newRoute(addLocationList);
            this.proxy = newProxy;
            promise.complete();
        } catch (Exception e) {
            promise.fail(e);
        }
        return promise.future();
    }

    /**
     * 创建新路由route
     *
     * @param locationList 路由地址
     */
    private void newRoute(List<JRPLocation> locationList) {
        for (JRPLocation location : locationList) {
            String locationRegex = location.getLocation();
            //以"/"结束的代理是前缀匹配代理服务下多个接口，非"/"结束的是代理的接口地址
            if (location.getProxy_pass().endsWith(SEPARATOR)) {
                boolean isRegexLocation = location.getRegex();
                if (!isRegexLocation) {
                    //普通前缀匹配转正则
                    locationRegex = locationRegex + ".*";
                }
            }
            HttpClientOptions clientOptions = new HttpClientOptions();
            boolean https = location.getProxy_pass().toLowerCase().startsWith(HTTPS);
            clientOptions.setSsl(https).setTrustAll(https);
            HttpClient proxyClient = vertx.createHttpClient(clientOptions);
            HttpProxy httpProxy = new JRPReverseProxy(location, new ProxyOptions(), proxyClient);
            Route route = proxyRouter.routeWithRegex(locationRegex);
            locationRouteMap.put(location.toString(), route);
            route.handler(ProxyHandler.create(httpProxy));
        }
    }

    private Future<Void> startTCPProxy() {
        Promise<Void> promise = Promise.promise();
        NetServer httpServer = vertx.createNetServer();
        NetClient client = vertx.createNetClient();
        httpServer.connectHandler((netSocket -> {
            netSocket.pause();
            String proxyPass = proxy.getProxy_pass();
            HostAndPort hostAndPort = HostAndPort.parseAuthority(proxyPass, 80);
            SocketAddress socketAddress = SocketAddress.inetSocketAddress(hostAndPort.port(), hostAndPort.host());
            client.connect(socketAddress).onSuccess(originSocket -> {
                originSocket.pause();
                originSocket.pipeTo(netSocket);
                netSocket.pipeTo(originSocket);
                originSocket.closeHandler(close -> netSocket.close());
                netSocket.closeHandler(close -> originSocket.close());
                netSocket.resume();
                originSocket.resume();
            }).onFailure(error -> {
                logger.error("TCP代理异常：{}", error.getMessage(), error);
                netSocket.close();
            });
        })).listen(proxy.getListen()).onComplete((rs) -> {
            if (rs.succeeded()) {
                promise.complete();
                logger.info("Proxy by type {} start on port {} success!", proxy.getType().name(), proxy.getListen());
            } else {
                logger.error("Proxy by type {} start on port {} error:{}!", proxy.getType().name(), proxy.getListen(), rs.cause().getMessage(), rs.cause());
                promise.fail(rs.cause());
            }
        });
        return promise.future();
    }

    private Future<Void> startUDPProxy() {
        Promise<Void> promise = Promise.promise();
        DatagramSocket datagramSocket = vertx.createDatagramSocket();
        String proxyPass = proxy.getProxy_pass();
        HostAndPort hostAndPort = HostAndPort.parseAuthority(proxyPass, 80);
        DatagramSocket client = vertx.createDatagramSocket();
        datagramSocket.handler(packet -> {
            client.handler(originPacket -> {
                SocketAddress sender = packet.sender();
                datagramSocket.send(originPacket.data(), sender.port(), sender.host()).onSuccess(aVoid -> logger.debug("返回数据到[{}]成功！", sender)).onFailure(error -> logger.error("返回数据到[{}]失败：{}！", sender, error.getMessage(), error.getCause()));
            });
            client.send(packet.data(), hostAndPort.port(), hostAndPort.host()).onSuccess(aVoid -> logger.debug("转发数据到[{}]成功！", hostAndPort)).onFailure(error -> logger.error("转发数据到[{}]失败：{}！", hostAndPort, error.getMessage(), error.getCause()));
        });
        datagramSocket.listen(proxy.getListen(), "*").onComplete((rs) -> {
            if (rs.succeeded()) {
                logger.info("Proxy of type {} start on port {} success!", proxy.getType().name(), proxy.getListen());
                promise.complete();
            } else {
                logger.error("Proxy of type {} start on port {} error:{}!", proxy.getType().name(), proxy.getListen(), rs.cause().getMessage(), rs.cause());
                promise.fail(rs.cause());
            }
        });
        return promise.future();
    }

    private Future<Void> startSOCKS4Proxy() {
        Promise<Void> promise = Promise.promise();
        NetServer netServer = vertx.createNetServer();
        NetClient client = vertx.createNetClient();
        netServer.connectHandler((netSocket -> {
            netSocket.pause();
            String proxyPass = proxy.getProxy_pass();
            HostAndPort hostAndPort = HostAndPort.parseAuthority(proxyPass, 80);
            SocketAddress socketAddress = SocketAddress.inetSocketAddress(hostAndPort.port(), hostAndPort.host());
            client.connect(socketAddress).onSuccess(originSocket -> {
                originSocket.pause();
                originSocket.pipeTo(netSocket);
                netSocket.pipeTo(originSocket);
                originSocket.closeHandler(close -> netSocket.close());
                netSocket.closeHandler(close -> originSocket.close());
                netSocket.resume();
                originSocket.resume();
            }).onFailure(error -> {
                logger.error("SOCKS4代理异常：{}", error.getMessage(), error);
                netSocket.close();
            });
        })).listen(proxy.getListen()).onComplete((rs) -> {
            if (rs.succeeded()) {
                logger.info("Proxy by type SOCKS4 start on port {} success!", proxy.getListen());
                promise.complete();
            } else {
                logger.error("Proxy by type SOCKS4 start on port {} error:{}!", proxy.getListen(), rs.cause().getMessage(), rs.cause());
                promise.fail(rs.cause());
            }
        });
        return promise.future();
    }

    private Future<Void> startSOCKS5Proxy() {
        Promise<Void> promise = Promise.promise();
        NetServer httpServer = vertx.createNetServer();
        NetClient client = vertx.createNetClient();
        httpServer.connectHandler((netSocket -> {
            netSocket.pause();
            String proxyPass = proxy.getProxy_pass();
            HostAndPort hostAndPort = HostAndPort.parseAuthority(proxyPass, 80);
            SocketAddress socketAddress = SocketAddress.inetSocketAddress(hostAndPort.port(), hostAndPort.host());
            client.connect(socketAddress).onSuccess(originSocket -> {
                originSocket.pause();
                originSocket.pipeTo(netSocket);
                netSocket.pipeTo(originSocket);
                originSocket.closeHandler(close -> netSocket.close());
                netSocket.closeHandler(close -> originSocket.close());
                netSocket.resume();
                originSocket.resume();
            }).onFailure(error -> {
                logger.error("SOCKS5代理异常：{}", error.getMessage(), error);
                netSocket.close();
            });
        })).listen(proxy.getListen()).onComplete((rs) -> {
            if (rs.succeeded()) {
                logger.info("Proxy by type SOCKS5 start on port {} success!", proxy.getListen());
                promise.complete();
            } else {
                logger.error("Proxy by type SOCKS5 start on port {} error:{}!", proxy.getListen(), rs.cause().getMessage(), rs.cause());
                promise.fail(rs.cause());
            }
        });
        return promise.future();
    }

    public String key() {
        return proxy.key();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ReverseProxyVerticle verticle = (ReverseProxyVerticle) o;
        return Objects.equals(verticle.key(), this.key());
    }

    @Override
    public int hashCode() {
        return Objects.hash(proxy.getType(), proxy.getCert_cer(), proxy.getCert_key(), proxy.getListen(), proxy.getProxy_pass());
    }
}
